[神奇的JS] 定时器校准
在实际编码中,我们总会使用setTimeout和setInterval去创建定时任务,如果对于时间精准度不高,满足业务要求还可,但是对于高时间精度的业务,我们还是需要一个相对精准的时钟。
创建定时器
使用JS中常用的两个方法
1 | console.log(performance.now()); |
这就可创建定时任务,我们假设让他在1s后执行,那么,事实上,真的是1s后便会执行任务吗?
任务的开始时间13696978.69,循环定时器执行时间期望应该是13696978.69 + 1000 = 13697978.69,实际为13697980.179,偏差2ms左右,延迟定时器执行时间期望应该是13696978.69 + 1900 = 13698878.69,实际为13698884.665,偏差6ms左右。
定时器偏差
为何出现偏差呢?在解释偏差出现的原因之前,不得不提及js的事件循环(event loop)机制。
在js的单线程执行栈中,任务被划分为同步任务和异步任务,而异步任务又分为微任务和宏任务,同步任务是处在‘第一梯队’中优先顺序执行;微任务处于‘第二梯队’,例如es6中的promise就是微任务的典型代表;最后便是宏任务,setTimeout和setInterval便是处在这个位置。
当我们定义一个定时器时,实际上也只是向事件循环队列的队尾处推送了一个任务而已,除非当前事件队列为空,否则不会立即执行,于是便产生了偏差,而我们也注意到,这个偏差只可能为正数。
校准定时器
知己知彼,百战不die。知道原因,如何校准方法如下:
- 防止setInterval的累计误差
这个阶段下,每次执行定时器任务前计算出误差,然后使用setTimeout推送事件。
1 | let taskId = null; |
这样可避免累计误差,相对校准定时器。
- 利用帧同步机制校准定时器
首先,实现一个帧时钟
1 | let step = (timestamp, elapsed) => { |
然后,加上任务
1 | let duration = 1000; |
反正挺准的。
扩展
提到了事件循环,那么有这样一段代码,他的输出是怎样的?
1 | console.log('aaa'); |
想出结果了吗?
处理Promise的语法糖async和await,async声明微任务,而await是具体的推送微任务,声明微任务的代码在真正推送微任务前是同步的。可以认为await == then
console.log(111) == 同步任务
await console.log(222) == 推送到事件队列一个微任务并且执行同步代码,从这时起微任务开始生效,js进程阻塞执行之后的‘同步代码’
归类:
同步代码
console.log(‘aaa’);
console.log(111);
await console.log(222);
console.log(‘bbb’);
微任务
console.log(333);
console.log(444);
宏任务
setTimeout(()=>console.log(‘t1’), 0);
setTimeout(() => console.log(‘t2’), 0);